package core;

import java.util.Calendar;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import common.TimeSource;

import core.exception.IllegalAddException;
import core.exception.IllegalRemoveException;
import core.reports.IReportVisitor;

/**
 * ItemManager houses a collection of Item objects. This is a Singleton class.
 * 
 * @author Justin
 */
public class ItemManager extends ModelObject {
	private static final long serialVersionUID = 6285562585539482489L;
	/** The singleton instance of the ItemManager */
	private static ItemManager _instance;
	/** A collection of Items */
	private Set<Item> _items;
	/** A collection of removed Items */
	private Set<Item> _removedItems;
	/** A Map from an Item Barcode to the actual Item */
	private Map<Barcode, Item> _barcodeToItem;
	/** A Map from a Product Barcode to the Items which are part of that Product */
	private Map<Product, Set<Item>> _productToItems;
	/** A Map from a Container to the Items which are part of that Container */
	private Map<Container, Set<Item>> _containerToItems;
	/** A Map from an Item Barcode to the Container where it's stored */
	private Map<Item, Container> _itemToContainer;
	/** An Int which represents the next available unique Barcode */
	private long _nextBarcode;
	
	
	/**
	 * The public static method for creating or getting the singleton instance of this class
	 * @return A new ItemManager if none exists or the existing one
	 */
	public static ItemManager instance(){
		if (_instance == null) {
			_instance = new ItemManager();
		}
		return _instance;
	}
	
	
	/**
	 * The public static method for replacing the current instance of this singleton class
	 * @param newManager a new instance of the ItemManager class
	 */
	public static void setInstance(ItemManager newManager){
		_instance = newManager;
	}
	
	/**
	 * Private ItemManager constructor
	 */
	private ItemManager(){
		//initialize private variables
		_items = new HashSet<Item>();
		_removedItems = new HashSet<Item>();
		_barcodeToItem = new HashMap<Barcode, Item>();
		_productToItems = new HashMap<Product, Set<Item>>();
		_containerToItems = new HashMap<Container, Set<Item>>();
		_itemToContainer = new HashMap<Item, Container>();
		_nextBarcode = 40000000000L;
	}

	/**
	 * Find an Item by its unique barcode
	 * @param barcode
	 * @return The item which has that specific barcode. Return NULL if the Item doesn't exist
	 */
	public Item lookupByBarcode(Barcode barcode){
		return this._barcodeToItem.get(barcode);
	}	
	
	/**
	 * Create an Item in the specified container
	 * @param product 				The product from which the Item will be created
	 * @param container 			The container where the Item should be located
	 * @return 						The Item that was created
	 * @throws IllegalAddException 	Thrown if the Item cannot be placed in the specified container
	 */
	public Item createItem(Product product, Container container) throws IllegalAddException{
		//create the item
		Barcode _tempBarcode = new Barcode(String.valueOf(_nextBarcode++), BarcodeType.ITEM);
		Item newItem = new Item(product, _tempBarcode);
		newItem.setManager(this);
		
		//if it's not already in the set, add it. Otherwise, throw an exception
		if(_items.add(newItem)){
			_barcodeToItem.put(_tempBarcode, newItem);
			addToProductToItemsMap(product, newItem);
			addToContainerToItemsMap(newItem, container);
			_itemToContainer.put(newItem, container);
			return newItem;
		}
		else{
			throw new IllegalAddException("Error in create item");
		}
	}

	
	/**
	 * Add a set of already-created items to the ItemManager. This method was created for undo/redo
	 * functionality
	 * @param items
	 */
	public void addItems(List<Item> items){
		Iterator<Item> iter = items.iterator();
		while(iter.hasNext()){
			Item tempItem = iter.next();
			addItem(tempItem);
		}
	}
	
	
	/**
	 * Add an already-created item to the ItemManager. This method was created for DBAllDAO
	 * @param item
	 */
	public void addItem(Item item){
		//add the item to:
		//_items
		_items.add(item);
		//_barcodeToItem
		_barcodeToItem.put(item.getBarcode(), item);
		//_productToItems
		addToProductToItemsMap(item.getProduct(), item);
		//_containerToItems
		addToContainerToItemsMap(item, null);
		//_itemToContainer
		_itemToContainer.put(item, item.getContainer());
	}

	/**
	 * Adds an entry to the ContainerToItems map
	 * @param container
	 * @param newItem
	 */
	public void addToItemToContainerMap(Container container, Item newItem) {
		if(!_itemToContainer.containsKey(newItem)){
			_itemToContainer.put(newItem, container);
		}
		//change it from null to the new container
		else if(_itemToContainer.get(newItem) == null){
			_itemToContainer.remove(newItem);
			_itemToContainer.put(newItem, container);
		}
	}

	/**
	 * Add an entry to the ContainerToItems map
	 * @param item
	 * @param newContainer
	 */
	public void addToContainerToItemsMap(Item item, Container newContainer) {
		if (_containerToItems.containsKey(newContainer)){
			_containerToItems.get(newContainer).add(item);
		}
		else{
			Set<Item> tempSet = new HashSet<Item>();
			tempSet.add(item);
			this._containerToItems.put(newContainer, tempSet);
		}
	}

	/**
	 * Adds an entry to the ProductToItems Map
	 * @param product
	 * @param newItem
	 */
	private void addToProductToItemsMap(Product product, Item newItem) {
		if(_productToItems.containsKey(product)){
			_productToItems.get(product).add(newItem);
		}
		else{
			Set<Item> tempSet = new HashSet<Item>();
			tempSet.add(newItem);
			_productToItems.put(product, tempSet);
		}
	}
	
	/**
	 * Gets the container for a given item
	 * 
	 * @param item the item
	 * 
	 * @return the associated container
	 */
	public final Container getContainer(Item item){
		return _itemToContainer.get(item);
	}
	
	
	/**
	 * Set the Container for a specific Item
	 * @param item
	 * @param newContainer
	 */
	public void setContainer(Item item, Container newContainer){
		Container _oldContainer = this.getContainer(item);
		_itemToContainer.remove(item);
		_itemToContainer.put(item, newContainer);
		
		//update _containerToItem
		_containerToItems.get(_oldContainer).remove(item);
		addToContainerToItemsMap(item, newContainer);
	}
	
	/**
	 * Remove an Item (but Item may still be in system)
	 * @param removedItem 				the Item to remove
	 * @throws IllegalRemoveException 	Thrown if the Item cannot be removed from its container
	 */
	public Item removeItem(Item removedItem) throws IllegalRemoveException{
		if(!_items.contains(removedItem)) return null;
		if(_removedItems.contains(removedItem)) return null;
		//set the exitDate to the current date
		removedItem.setExitDate(TimeSource.instance().getTime());
		
		_removedItems.add(removedItem);
		return removedItem;
	}
	
	/**
	 * Unremove an item.
	 */
	protected void unRemoveItem(Item item, Container container){
		assert(_removedItems.contains(item));
		item.setExitDate(null);
		
		_removedItems.remove(item);
	}

	/**
	 * Move an Item from one container to another
	 * @param item
	 * @param oldContainer
	 * @param newContainer
	 */
	private void moveItem(Item item, Container oldContainer, Container newContainer){
		//update the maps from container to item
		_containerToItems.get(oldContainer).remove(item);
		addToContainerToItemsMap(item, newContainer);
		_itemToContainer.remove(item);
		_itemToContainer.put(item, newContainer);
		item.setContainer(newContainer);
	}


	
	
	/**
	 * Get all the items that have been removed
	 * @param after
	 * @return
	 */
	public Iterator<Item> getRemovedItems(Calendar after){
		//return all items that were removed after the passed-in Calendar date
		Set<Item> tempSet = new TreeSet<Item>();
		Iterator<Item> iter = this._removedItems.iterator();
		while(iter.hasNext()){
			Item current = iter.next();
			if(current.getExitDate().compareTo(after) > 0){
				tempSet.add(current);
			}
		}
		return java.util.Collections.unmodifiableSet(tempSet).iterator();
	}
	
	
	/**
	 * Get all the expired items
	 * @param before
	 * @return
	 */
	public Iterator<Item> getExpiredItems(Calendar before){
		//return all items that expired before the passed-in Calendar date
		Set<Item> tempSet = new TreeSet<Item>();
		Iterator<Item> iter = _items.iterator();
		while(iter.hasNext()){
			Item current = iter.next();
			if(!_removedItems.contains(current) &&
					(current.getExpirationDate()).compareTo(before) < 0){
				tempSet.add(current);
			}
		}
		return java.util.Collections.unmodifiableSet(tempSet).iterator();
	}
	
	/**
	 * Completely remove all references to a given Container
	 * @param containers
	 */
	public void removeReferencesToContainer(Collection<Container> containers){
		Iterator<Container> iter = containers.iterator();
		//remove references these containers from the maps
		while(iter.hasNext()){
			this._containerToItems.remove(iter.next());
		}
	}
	
	/**
	 * Set a new Container for all Items of a given Product
	 * @param oldContainer
	 * @param product
	 * @param newContainer
	 */
	public void changeContainerForProduct(Container oldContainer, Product product, 
			Container newContainer){
		//get the items in oldContainer and product and set their container to newContainer
		//get the items in oldContainer
		//for each item in oldContainer
		HashSet<Item> _toReparent = new HashSet<Item>();
		if (_containerToItems.get(oldContainer) == null)
			return; // no items for this product and container to move anyway.
		Iterator<Item> iter = _containerToItems.get(oldContainer).iterator();
		while(iter.hasNext()){
			//if the product matches
			Item _tempItem = iter.next();
			if(_tempItem.getProduct().equals(product)){
				//change its container to newContainer
				//remove this item from _containerToItems.get(oldContainer)
				//add this item to _containerToItems.get(newContainer)
				_toReparent.add(_tempItem);
			}
		}
		Iterator<Item> itemIter = _toReparent.iterator();
		while(itemIter.hasNext()){
			Item _next = itemIter.next();
			_next.setContainer(newContainer);
		}
	}
	
	/**
	 * Get a list of Items that have not been removed of a given Product
	 * @param product
	 * @return
	 */
	public Iterator<Item> getItemsInProduct(Product product){
		Set<Item> tempSet = new HashSet<Item>();
		if(!_productToItems.containsKey(product))return null;
		Iterator<Item> iter = _productToItems.get(product).iterator();
		while(iter.hasNext()){
			Item tempItem = iter.next();
			if (tempItem != null && !_removedItems.contains(tempItem))
				tempSet.add(tempItem);
		}
		return java.util.Collections.unmodifiableSet(tempSet).iterator();
	}
	
	/**
	 * Gets all the items for the product, including removed items
	 * 
	 * @param product the product
	 * @return iterator for the items
	 */
	public Iterator<Item> getAllItemsInProduct(Product product){
		Set<Item> tempSet = new HashSet<Item>();
		if(!_productToItems.containsKey(product))return null;
		Iterator<Item> iter = _productToItems.get(product).iterator();
		while(iter.hasNext()){
			Item tempItem = iter.next();
			if (tempItem != null)
				tempSet.add(tempItem);
		}
		return java.util.Collections.unmodifiableSet(tempSet).iterator();
	}
	
	/**
	 * Get an Iterator to a list of Items of a given Product in a given Container
	 * @param container
	 * @param product
	 * @return
	 */
	public Iterator<Item> itemsInProductAndContainer(Container container, Product product){
		Set<Item> tempSet = getItemsInProductAndContainer(container, product);
		if (tempSet == null)
			return null;
		return java.util.Collections.unmodifiableSet(tempSet).iterator();
	}


	/**
	 * Get a list of Items of a given Product in a given Container
	 * @param container
	 * @param product
	 * @return
	 */
	private Set<Item> getItemsInProductAndContainer(Container container,
			Product product) {
		Set<Item> tempSet = new TreeSet<Item>();
		if(!_containerToItems.containsKey(container))return null;
		Iterator<Item> iter = _containerToItems.get(container).iterator();
		while(iter.hasNext()){
			Item _tempItem = iter.next();
			if(_tempItem.getProduct().equals(product) && !_removedItems.contains(_tempItem)){
				tempSet.add(_tempItem);
			}
		}
		return tempSet;
	}
	
	/**
	 * Get the number of items of a given Product in a given Container
	 * @param container
	 * @param product
	 * @return
	 */
	public int numItemsInProductAndContainer(Container container, Product product){
		if(!_containerToItems.containsKey(container)) return 0;
		Set<Item> set = getItemsInProductAndContainer(container, product);
		return set.size();
	}
	
	/**
	 * @param container A Container that stores items
	 * @return a protected Iterator to a list of Item objects within that container
	 */
	public Iterator<Item> itemsInContainer(Container container){
		Set<Item> tempSet = new TreeSet<Item>();
		if(!_containerToItems.containsKey(container)){
			return null;
		}
		Iterator<Item> iter = _containerToItems.get(container).iterator();
		while(iter.hasNext()){
			Item temp = iter.next();
			if (!_removedItems.contains(temp))
				tempSet.add(temp);
		}
		return java.util.Collections.unmodifiableSet(tempSet).iterator();
	}

	/**
	 * Completely remove an Item from everywhere in the system
	 * @param item
	 */
	public void removeItemFromSystem(Item item){
		assert item != null;
		//remove it from everywhere in the system
		Product tempProduct = item.getProduct();
		Container tempContainer = item.getContainer();
		//check _items
		_items.remove(item);
		//check _removedItems
		_removedItems.remove(item);
		//check _barcodeToItem
		_barcodeToItem.remove(item.getBarcode());
		//check _productToItems
		if(_productToItems.get(tempProduct) != null){
			_productToItems.get(tempProduct).remove(item);
		}
		
		//check _containerToItems
		if(_containerToItems.get(tempContainer) != null){
			_containerToItems.get(tempContainer).remove(item);
		}
		//check _itemToContainer
		_itemToContainer.remove(item);
	}
	
	/**
	 * Removes all the items for a product from the system
	 * 
	 * @param product the product 
	 * 
	 * @pre product != null
	 */
	protected void removeAllItemsForProduct(Product product){
		assert product != null;
		//find the items in _productToItems
		Iterator <Item> iter = this.getItemsInProduct(product);
		//for each item
		if (iter == null)
			return;
		while (iter.hasNext())
		{
			removeItemFromSystem(iter.next());
		}
	}
	
	/**Add an item to the container. If the item's product is held in
	 * a subcontainer within this the container, the item is added to the subcontainer.
	 * If the product is not yet in the storage unit, the product is added
	 * to the Container, and then the item is added.
	 * 
	 * @param item The item to be added.
	 */
	protected void addItemToContainer(Container container, Item item){
		// check preconditions
		assert(container != null && item != null);
	
		// Get the item's product
		Product product = item.getProduct();
		
		// Determine where the item should go (if product already in storage
		// unit, it goes with product, otherwise it goes here).
		Container oldContainer = container.getStorageUnit().getContainerForProduct(product);
		Container addHere = container;
		
		if (oldContainer != null){
			addHere = oldContainer;
		}
		else {
			// Add the product to this container
			ProductManager.instance().addProductToContainer(product, container);
		}
		
		// Tell the item where it belongs
		moveItem(item, item.getContainer(), addHere);
	}
	
	/**Transfer the given item to the container. This effectively removes the
	 * item from it's old container and adds it to the given Container. 
	 * 
	 * @param item The item to transfer to this Container.
	 * @param container The storage unit to transfer this item to.
	 */
	protected void transferItem(Container container, Item item){
		// Check preconditions
		assert(item != null && container != null);
		
		// Transferring is the same as adding, because adding automatically
		// sets the item's container whether or not the item was already
		// contained somewhere else, effectively removing it from it's old
		// location.
		addItemToContainer(container, item);
	}	
	
	/**Move the given item to this container. This is different than adding
	 * or transferring an item because adding or transferring puts the item
	 * in a subcontainer if a subcontainer has that product. Moving an item
	 * will also move the product and all other items of that product out of
	 * a subcontainer into the given container. If the product is not currently 
	 * in the storage unit, the behavior is the same as transferItem. 
	 * 
	 * @param item The item to move into this container.
	 * @param container The container to move the item to.
	 * @throws IllegalAddException 
	 */
	protected void moveItem(Container container, Item item) throws IllegalAddException {
		// Check preconditions
		assert(container != null && item != null);
		
		// If the item's product is already stored elsewhere in this storage unit
		// than move it here
		if (container.getStorageUnit().getContainerForProduct(item.getProduct()) != container)
			ProductManager.instance().addProductToContainer(item.getProduct(), container);
			
		// now calling addItem will add it here and take care of the rest.
		ItemManager.instance().addItemToContainer(container, item);
	}	
	
	/**
	 * 
	 * @return all items that have not been removed
	 */
	protected Iterator<Item> getAllItems() {
		return java.util.Collections.unmodifiableSet(_items).iterator();
	}
	
	/**
	 * Accepts a Visitor
	 * @param v the visiting IReportVisitor
	 */
	public void accept(IReportVisitor v) {
		// TODO Auto-generated method stub
		v.visitItemManager(this);
	}
	
	protected void decrementBarcode() {
		_nextBarcode--;
	}
	
	public void setNextBarcode(long next)
	{
		_nextBarcode = next;
	}
}
